Leaflet Blog in Deno Fresh
1/** @jsxImportSource preact */
2import { CSS, render } from "@deno/gfm";
3import { Handlers, PageProps } from "$fresh/server.ts";
4
5import { Layout } from "../../islands/layout.tsx";
6import { PostInfo } from "../../components/post-info.tsx";
7import { Title } from "../../components/typography.tsx";
8import { getPost } from "../../lib/api.ts";
9import { Head } from "$fresh/runtime.ts";
10
11interface Post {
12 uri: string;
13 value: {
14 title?: string;
15 subtitle?: string;
16 content?: string;
17 createdAt?: string;
18 };
19}
20
21// Only override backgrounds in dark mode to make them transparent
22const transparentDarkModeCSS = `
23@media (prefers-color-scheme: dark) {
24 .markdown-body {
25 color: white;
26 background-color: transparent;
27 }
28
29 .markdown-body a {
30 color: #58a6ff;
31 }
32
33 .markdown-body blockquote {
34 border-left-color: #30363d;
35 background-color: transparent;
36 }
37
38 .markdown-body pre,
39 .markdown-body code {
40 background-color: transparent;
41 color: #c9d1d9;
42 }
43
44 .markdown-body table td,
45 .markdown-body table th {
46 border-color: #30363d;
47 background-color: transparent;
48 }
49}
50
51.font-sans { font-family: var(--font-sans); }
52.font-serif { font-family: var(--font-serif); }
53.font-mono { font-family: var(--font-mono); }
54
55.markdown-body h1 {
56 font-family: var(--font-serif);
57 text-transform: uppercase;
58 font-size: 2.25rem;
59}
60
61.markdown-body h2 {
62 font-family: var(--font-serif);
63 text-transform: uppercase;
64 font-size: 1.75rem;
65}
66
67.markdown-body h3 {
68 font-family: var(--font-serif);
69 text-transform: uppercase;
70 font-size: 1.5rem;
71}
72
73.markdown-body h4 {
74 font-family: var(--font-serif);
75 text-transform: uppercase;
76 font-size: 1.25rem;
77}
78
79.markdown-body h5 {
80 font-family: var(--font-serif);
81 text-transform: uppercase;
82 font-size: 1rem;
83}
84
85.markdown-body h6 {
86 font-family: var(--font-serif);
87 text-transform: uppercase;
88 font-size: 0.875rem;
89}
90`;
91
92export const handler: Handlers<Post> = {
93 async GET(_req, ctx) {
94 try {
95 const { slug } = ctx.params;
96 const post = await getPost(slug);
97 return ctx.render(post);
98 } catch (error) {
99 console.error("Error fetching post:", error);
100 return new Response("Post not found", { status: 404 });
101 }
102 },
103};
104
105export default function BlogPage({ data: post }: PageProps<Post>) {
106 if (!post) {
107 return <div>Post not found</div>;
108 }
109
110 return (
111 <>
112 <Head>
113 <title>{post.value.title} — knotbin</title>
114 <meta
115 name="description"
116 content={post.value.subtitle || "by Roscoe Rubin-Rottenberg"}
117 />
118 {/* Merge GFM's default styles with our dark-mode overrides */}
119 <style
120 dangerouslySetInnerHTML={{ __html: CSS + transparentDarkModeCSS }}
121 />
122 </Head>
123
124 <Layout>
125 <div class="p-8 pb-20 gap-16 sm:p-20">
126 <link rel="alternate" href={post.uri} />
127 <div class="max-w-[600px] mx-auto">
128 <article class="w-full space-y-8">
129 <div class="space-y-4 w-full">
130 <Title>{post.value.title || 'Untitled'}</Title>
131 {post.value.subtitle && (
132 <p class="text-2xl md:text-3xl font-serif leading-relaxed max-w-prose">
133 {post.value.subtitle}
134 </p>
135 )}
136 <PostInfo
137 content={post.value.content || ''}
138 createdAt={post.value.createdAt || new Date().toISOString()}
139 includeAuthor
140 className="text-sm"
141 />
142 <div class="diagonal-pattern w-full h-3" />
143 </div>
144 <div class="[&>.bluesky-embed]:mt-8 [&>.bluesky-embed]:mb-0">
145 <div
146 class="mt-8 markdown-body"
147 // replace old pds url with new one for blob urls
148 dangerouslySetInnerHTML={{
149 __html: render(post.value.content || '').replace(
150 /puffball\.us-east\.host\.bsky\.network/g,
151 "knotbin.xyz",
152 ),
153 }}
154 />
155 </div>
156 </article>
157 </div>
158 </div>
159 </Layout>
160 </>
161 );
162}